use godot::{
    classes::{
        ArrayMesh, MeshInstance3D,
        mesh::{ArrayType, PrimitiveType},
    },
    prelude::*,
};

use crate::chunk::{self, Chunk};

#[derive(GodotClass)]
#[class(init, base=MeshInstance3D)]
pub struct ChunkInstance {
    base: Base<MeshInstance3D>,
    neighbours: [Option<Gd<Self>>; 6],
    chunk: Chunk,
    dirty: bool,
}

#[godot_api]
impl ChunkInstance {
    #[constant]
    pub const NEIGHBOR_RIGHT: usize = chunk::Neighbor::Right as usize;
    #[constant]
    pub const NEIGHBOR_UP: usize = chunk::Neighbor::Up as usize;
    #[constant]
    pub const NEIGHBOR_BACK: usize = chunk::Neighbor::Back as usize;
    #[constant]
    pub const NEIGHBOR_LEFT: usize = chunk::Neighbor::Left as usize;
    #[constant]
    pub const NEIGHBOR_DOWN: usize = chunk::Neighbor::Down as usize;
    #[constant]
    pub const NEIGHBOR_FORWARD: usize = chunk::Neighbor::Forward as usize;
    #[constant]
    pub const NEIGHBOR_MAX: usize = 6;

    pub fn get_neighbor_idx_raw(center: Vector3i, pos: Vector3i) -> Option<usize> {
        match pos - center {
            Vector3i::LEFT => Some(Self::NEIGHBOR_LEFT),
            Vector3i::RIGHT => Some(Self::NEIGHBOR_RIGHT),
            Vector3i::UP => Some(Self::NEIGHBOR_UP),
            Vector3i::DOWN => Some(Self::NEIGHBOR_DOWN),
            Vector3i::BACK => Some(Self::NEIGHBOR_BACK),
            Vector3i::FORWARD => Some(Self::NEIGHBOR_FORWARD),
            _ => None,
        }
    }

    #[func]
    pub fn get_neighbor_idx(center: Vector3i, pos: Vector3i) -> Variant {
        Self::get_neighbor_idx_raw(center, pos)
            .map(|x| Variant::from(x as i64))
            .unwrap_or_default()
    }

    #[func(gd_self)]
    pub fn set_voxel(mut this: Gd<Self>, pos: Vector3i, color: Color) {
        this.bind_mut().chunk.set_voxel(pos, color);

        Self::notify_neighbors(this.clone(), pos);
        this.bind_mut().dirty = true;
    }

    #[func(gd_self)]
    pub fn remove_voxel(mut this: Gd<Self>, pos: Vector3i) {
        this.bind_mut().chunk.remove_voxel(pos);

        Self::notify_neighbors(this.clone(), pos);
        this.bind_mut().dirty = true;
    }

    fn notify_neighbors(this: Gd<Self>, pos: Vector3i) {
        let (x_end, y_end, z_end) = {
            let this = this.bind();
            (
                this.chunk.get_width() - 1,
                this.chunk.get_height() - 1,
                this.chunk.get_length() - 1,
            )
        };

        if pos.x == 0
            && let Some(mut neighbor) = { this.bind().get_neighbor_raw(Self::NEIGHBOR_LEFT) }
        {
            neighbor.bind_mut().recache(Self::NEIGHBOR_RIGHT);
        } else if pos.x == x_end
            && let Some(mut neighbor) = { this.bind().get_neighbor_raw(Self::NEIGHBOR_RIGHT) }
        {
            neighbor.bind_mut().recache(Self::NEIGHBOR_LEFT);
        }

        if pos.y == 0
            && let Some(mut neighbor) = { this.bind().get_neighbor_raw(Self::NEIGHBOR_DOWN) }
        {
            neighbor.bind_mut().recache(Self::NEIGHBOR_UP);
        } else if pos.y == y_end
            && let Some(mut neighbor) = { this.bind().get_neighbor_raw(Self::NEIGHBOR_UP) }
        {
            neighbor.bind_mut().recache(Self::NEIGHBOR_DOWN);
        }

        if pos.z == 0
            && let Some(mut neighbor) = { this.bind().get_neighbor_raw(Self::NEIGHBOR_FORWARD) }
        {
            neighbor.bind_mut().recache(Self::NEIGHBOR_BACK);
        } else if pos.z == z_end
            && let Some(mut neighbor) = { this.bind().get_neighbor_raw(Self::NEIGHBOR_BACK) }
        {
            neighbor.bind_mut().recache(Self::NEIGHBOR_FORWARD);
        }
    }

    pub fn neighbor_mut(&mut self, idx: impl Into<chunk::Neighbor>) -> &mut Option<Gd<Self>> {
        &mut self.neighbours[idx.into() as usize]
    }

    pub fn get_neighbor_raw(&self, idx: impl Into<chunk::Neighbor>) -> Option<Gd<Self>> {
        self.neighbours[idx.into() as usize].clone()
    }

    pub fn recache(&mut self, neighbor_idx: impl Into<chunk::Neighbor> + Copy) {
        if let Some(neighbor) = self.get_neighbor_raw(neighbor_idx) {
            self.chunk
                .cache_neighbor(neighbor_idx, &neighbor.bind().chunk);
            self.dirty = true;
        }
    }

    #[func]
    pub fn set_neighbor(&mut self, neighbor_idx: i64, mut neighbor: Gd<Self>) {
        self.neighbor_mut(neighbor_idx).replace(neighbor.clone());
        neighbor
            .bind_mut()
            .neighbor_mut(neighbor_idx + 3)
            .replace(self.to_gd());

        self.chunk
            .cache_neighbor(neighbor_idx, &neighbor.bind().chunk);

        godot_print!("Caching {:?}", chunk::Neighbor::from(neighbor_idx));

        neighbor
            .bind_mut()
            .chunk
            .cache_neighbor(neighbor_idx + 3, &self.chunk);

        self.dirty = true;
    }

    #[func]
    pub fn remove_neighbor(&mut self, neighbor_idx: i64) {
        self.neighbor_mut(neighbor_idx).take();
        self.chunk.clear_neighbor_cache(neighbor_idx);

        self.dirty = true;
    }

    #[func]
    pub fn get_voxel(&self, pos: Vector3i) -> Variant {
        self.chunk
            .get_voxel(pos)
            .map(|x| x.to_variant())
            .unwrap_or_default()
    }

    #[func]
    pub fn rebake(&mut self) -> bool {
        if !self.dirty {
            return false;
        }

        let mut mesh = self
            .base()
            .get_mesh()
            .map(|m| m.cast())
            .unwrap_or(ArrayMesh::new_gd());

        mesh.clear_surfaces();

        let _ = defer::defer(|| {
            self.base_mut().set_mesh(&mesh);
            self.dirty = false;
        });

        let (quads, normals, colors) = self.chunk.greedy_meshing();

        if quads.is_empty() || normals.is_empty() || colors.is_empty() {
            return true;
        }

        let vertices: PackedVector3Array = quads
            .iter()
            .flat_map(|v| v)
            .map(|x| x.cast_float())
            .collect();

        let indices: PackedInt32Array = (0..quads.len())
            .flat_map(|idx| {
                let begin = idx as i32 * 4;
                [begin, begin + 1, begin + 2, begin, begin + 2, begin + 3]
            })
            .collect();

        let normals: PackedVector3Array = normals
            .iter()
            .flat_map(|x| core::iter::repeat_n(x.cast_float(), 4))
            .collect();

        let colors: PackedColorArray = colors
            .iter()
            .flat_map(|x| core::iter::repeat_n(x.clone(), 4))
            .collect();

        let mut arrays = Array::new();
        arrays.resize(ArrayType::MAX.ord() as _, &Variant::nil());

        arrays.set(ArrayType::VERTEX.ord() as _, &vertices.to_variant());
        arrays.set(ArrayType::INDEX.ord() as _, &indices.to_variant());
        arrays.set(ArrayType::NORMAL.ord() as _, &normals.to_variant());
        arrays.set(ArrayType::COLOR.ord() as _, &colors.to_variant());

        mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &arrays);

        true
    }
}
